iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 21
0

我們在之前的文章中已經花了不少的篇幅來探索Flutter的三顆渲染樹是怎麼運作的,不過其實這也只是整個Flutter Render Pipeline的一部分而已。整個Pipeline從接收到GPU傳來的vsync訊號,到最後產生一個frame給GPU進行繪製,中間到底經過了哪些步驟,今天就讓我們來一探究竟。

流程圖

如果你稍微用Google搜尋一下Flutter Render Pipeline相關文章,大概都會在文章中看到這三張圖的其中一張:
https://ithelp.ithome.com.tw/upload/images/20200921/20129053IXfWYLEE2r.png
(圖一:來源

https://ithelp.ithome.com.tw/upload/images/20200921/20129053ZALeEPD9fu.jpg
(圖二:來源

https://ithelp.ithome.com.tw/upload/images/20200921/20129053BvRTgloagE.png
(圖三:來源

這三張圖分開來看的話,可能會讓人有點困惑,每張圖中有點像又不太一樣的流程,到底是怎麼搭配起來的?不過一旦找到這些圖的原始出處後,一切就很清楚了。圖一和圖二來自這場演講的投影片,其中圖二是圖一將Dart(UI Thread)部份展開的結果,也就是我們平常會接觸到的部份。圖三則是官方說明文件中,將整個流程圖省略Thread,並加上文字說明的結果。

當然如果這三張圖你都是第一次看到的話,可能覺得這是很理所當然容易理解的事。不過因為它們總是在其它Pipeline相關文章中單獨出現,一開始其實造成了我些許的困惑。

總之,我們先搭配這三張圖,把整個流程大致走一遍吧:

  1. GPU傳送Vsync訊號給Flutter Framework(這裡實際上會通過Flutter Engine)
  2. Flutter Framework在UI Thread(也就是main Isolate)上開始一系列的Animate, Build, Layout, Paint流程,最終產生出Layer Tree
  3. Layer Tree被傳送給GPU Thread的Compositor,將所有Layer組合起來
  4. 由Skia負責進行Rasterize
  5. 最後呼叫OpenGL或Vulken,由GPU將畫面繪製在螢幕上

接下來我們簡單解釋一下官方說明的七個步驟

User Input

User Input可能在任何一個vsync中間發生,它的來源除了一般的觸控螢幕之外,也可能是外接鍵盤、麥克風等等。Flutter框架在接收到這些事件之後,經過我們的應用程式邏輯,最終有可能造成state的改變,這時候相關的widget會被mark dirty,而它們就會在下一個vsync來時被重新繪製。

Animation

相對於User Input,Animation則是隨著vsync開始,在這一個vsync進行處理。vsync引發TickerProvider的一個Tick,Animation Widget進行對應的state改變,Widget被mark dirty後便開始進行繪製。舉例來說,當我們在進行scroll animation的時候,每一個vsync訊號都會使scrollPosition這個state改變一點點,在每個vsync不斷重新繪製scrollPosition改變的ScrollView,就產生了滾動的動畫效果。

Build

這就是我們最熟悉的build函數被執行的地方。在這裡我們會根據Widget設定,建出完整的Widget Tree、更新Element的Widget reference,最後搭配State來更新RenderObject。值得注意的是到這裡我們還不會知道這些RenderObject實際將被繪製的大小和位置,只知道它們的constraints,也就是min/max width, min/max height而已。

Layout

這其實是值得獨立一篇文章來討論的步驟,不過在這裡讓我們試著用最簡單的方式說明。

首先每個RenderObject都會收到來自parent的一些constraints,用來描述parent希望這個child的size符合某些規範。這些constraints可以是任意的形式,但在絕大多數情況下,我們使用的其實都是繼承RenderObject的RenderBox,也就是以min/max width, min/max height作為constraints的一種RenderObject。

當一個RenderObject收到來自parent的constraints之後,會再根據自己的設定,傳遞新的constraints給自己的child。例如說,假設Padding(8)收到了[8, 24], [8, 24]這樣的constraints,就會傳遞[8, 16], [8, 16]的constraints給child。

Contrainsts以Depth First的方式傳遞到Leaf Node時,該RenderObject便可以根據constraints和自己的preference,來決定自己的size,這些preference有可能是max width/height或min width/height。

child決定自己的size之後便回傳給parent,由parent根據每個child的size和自己的layout規則,來決定每個child的position。例如一個Row,收到child1的width是100,就可以決定child2的x position是100。同時,parent也會根據每個child最終的size,加上自己的preference,來決定自己的size,進一步向上傳遞。

整個流程是one pass depth first traversal,constraints不斷被傳遞下去,size不斷被傳遞回來並決定position。

Paint

這裡雖然叫做Paint,但還不是實際繪製到螢幕上,只是根據各種不同RenderObject的功能和設定,呼叫canvas進行draw,產生Layer並組合成Layer Tree而已。

Composition

到這裡就進入GPU Thread了,整個Layer Tree會根據draw order被合併成單一的圖層。

Rasterize

最後,合併的圖層被送到Skia進行柵格化,也就是將原本由許多三角形表示的圖層轉換成由真正的像素表示。這些像素最後將交由GPU繪製到螢幕上

結語

這次我們大致走過了Flutter繪製一個Frame的整個Pipeline,相信你也看得出來後面幾個步驟的解說越來越少了,除了時間的關係之外,後面越接近底層的步驟也就越難找到相關資料。如果未來有找到更多資訊的話,我們在另外補充吧。


上一篇
days[19] = "Event Loop是怎麼運作的?"
下一篇
days[21] = "Layout是怎麼運作的?"
系列文
Why Flutter why? 從表層到底層,從如何到為何。30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言